<?php
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2018\11\28 0028
 * Time: 16:55
 */

require_once "MongoModel.php";

class Ws
{
    const WS_IP = "0.0.0.0";
    const WS_PORT = 9501;

    const PROCESS_NAME = 'DH_WS';
    const REDIS_HOST = '127.0.0.1';
    const REDIS_PORT = 6379;
    const REDIS_PWD = '';
    const REDIS_INDEX = 1;

    //消息类型
    const MSG_TYPE_OPEN = 1;  //ws客户端连接消息
    const MSG_TYPE_SEND = 2;  //ws客户端发送消息
    const MSG_TYPE_IDENTITY = 3; //ws客户端身份消息
    const MSG_TYPE_ONLINE_NUMBER = 4; //服务器发送给客户端  在线人数消息
    const MSG_TYPE_DROP_LINE = 5;  //服务器发送给客户端， 掉线消息
    const MSG_TYPE_FILE = 6;  //文件消息
    const MSG_TYPE_CLOSE = 7;  //客服关闭会话的消息
    //const MSG_TYPE_SCREENSHOT = 8; //截图消息

    //连接用户的身份
    const NORMAL_IDENTITY = 1; //游客
    const VIP_IDENTITY = 2; //登录后的会员
    const CUSTOMER_PRE_IDENTITY = 3; //售前客服
    const CUSTOMER_AFTER_IDENTITY = 4; //售后客服

    //连接用户列表
    const GUEST_LIST = 'guest_list';
    const VIP_LIST = "vip_list";
    const CUSTOMER_PRE_LIST = "customer_pre_list";
    const CUSTOMER_AFTER_LIST = "customer_after_list";

    //任务池  类型
    const TASK_ADD_MONGO = "add_mongo";
    const TASK_ADD_IDENTITY = "add_identity";
    const TASK_DEL_IDENTITY = "del_identity";
    const TASK_CLOSE_SESSION = "close_session";
    const TASK_ADD_MYSQL = "add_mysql";

    //table 大小  （后期无法扩容，谨慎设置）
    const TABLE_SIZE = 1024;
    //单条消息不能超过1K
    const MESSAGE_MAX_LEN     = 1024;

    //在线人数  类型
    const ONLINE_TYPE = 1;              // 总在线人数
    const ONLINE_CUSTOMER = 2;          // 客服在线总人数
    const ONLINE_CUSTOMER_PRE = 3;      // 售前客服在线人数
    const ONLINE_CUSTOMER_AFTER = 4;    // 售后客服在线人数
    const ONLINE_USER = 5;              // 用户在线总人数
    const ONLINE_GUEST = 6;             // 游客在线人数
    const ONLINE_VIP = 7;               // 会员在线人数

    //finish 的类型
    const FINISH_GET_ONLINE = 1;
    const FINISH_DROP_LINE = 2;

    //会话发起类型
    const SESSION_GUEST_TO_CUSTOMER = 1;    //游客对客服发起
    const SESSION_VIP_TO_CUSTOMER = 2;      //会员对客服发起
    const SESSION_CUSTOMER_TO_GUEST = 3;    //客服对游客发起
    const SESSION_CUSTOMER_TO_VIP = 4;      //客服对会员发起
    const SESSION_VIP_TO_GUEST = 5;         //会员对游客发起
    const SESSION_GUEST_TO_VIP = 6;         //游客对会员发起

    //文字提示
    const CHARACTER_DESCRIPTION_CLOSE = '客服关闭了会话';

    private $swoole_client;
    public function __construct()
    {
        $this->swoole_client = new swoole_websocket_server(self::WS_IP, self::WS_PORT);

        //配置
        $this->swoole_client->set([
            'worker_num' => 2,
            'task_worker_num' => 4,
            'heartbeat_check_interval' => 5,
            'heartbeat_idle_time' => 10 * 60,
//            'document_root' => '/usr/local/nginx/html/swoole/data/chat',
//            'enable_static_handler' => true,
        ]);

        $table = new swoole_table(self::TABLE_SIZE);
        $table->column('fd', swoole_table::TYPE_INT);
        $table->column('type', swoole_table::TYPE_INT);
        $table->column('data', swoole_table::TYPE_STRING, 255);
        $table->create();
        $this->swoole_client->table = $table;

        $this->swoole_client->on('open', [$this, 'onOpen']);

        $this->swoole_client->on('task', [$this, 'onTask']);

        $this->swoole_client->on('finish', [$this, 'onFinish']);

        $this->swoole_client->on('message', [$this, 'onMessage']);

        $this->swoole_client->on('close', [$this, 'onClose']);

        $this->swoole_client->on('start', [$this, 'onStart']);

        $this->swoole_client->start();
    }

    public function onStart()
    {
        swoole_set_process_name(self::PROCESS_NAME);    //设置进程名
    }

    /**
     * 客户端关闭
     * @param $server
     * @param $fd
     */
    public function onClose($server, $fd)
    {
        $data = [
            'fd' => $fd,
            'task_type' => self::TASK_DEL_IDENTITY,
        ];
        $server->task(serialize($data));
        echo "client {$fd} closed\n";
    }

    /**
     * 处理客户端连接后的操作
     * @param $server
     * @param $request
     */
    public function onOpen($server, $request)
    {
        echo "server: handshake success with fd{$request->fd}\n";
    }

    /**
     * 处理客户端的消息
     * @param $server
     * @param $frame
     */
    public function onMessage($server, $frame)
    {
        $requestData = json_decode($frame->data, true);

        $data = $this->format_response_data($server, $requestData, $frame->fd);

        $server->push(intval($data['userFd']), json_encode($data));
    }

    /**
     * 处理任务池
     * @param $serv
     * @param $taskId
     * @param $workerId
     * @param $data
     * @throws \MongoDB\Driver\Exception\Exception
     */
    public function onTask($serv, $taskId, $workerId, $data)
    {
        $req = unserialize($data);
        if ($req)
        {
            switch($req['task_type'])
            {
                case self::TASK_ADD_MONGO:
                    $this->insert_mongodb($req['userFd'], $req['toUserFd'], $req['msg'], $req['msg_type']);
                    break;
                case self::TASK_ADD_IDENTITY:
                    unset($req['task_type']);
                    $req['data'] = serialize($req['data']);
                    $this->setIdentity($serv, $req['fd'], $req);
                    $finish_data = [
                        'type' => self::FINISH_GET_ONLINE,
                    ];
                    $serv->finish(serialize($finish_data));
                    break;
                case self::TASK_DEL_IDENTITY:
                    $type = $this->fdToType($serv, $req['fd']);
                    $this->delIdentity($serv, $req['fd'], $type);
                    if ($type == self::NORMAL_IDENTITY || $type == self::VIP_IDENTITY) {
                        $finish_data = [
                            'type' => self::FINISH_DROP_LINE,
                            'fd'   => $req['fd'],
                        ];
                        $serv->finish(serialize($finish_data));
                    }
                    break;
                default:
                    break;
            }
        }
    }

    /**
     * 执行task后调用
     * @param $server
     * @param $task_id
     * @param $data
     */
    public function onFinish($server, $task_id, $data)
    {
        $req = unserialize($data);
        switch ($req['type']) {
            case self::FINISH_GET_ONLINE:
                $number = $this->getNumbers(self::ONLINE_USER);
                $broadcast_data = [
                    'type' => self::MSG_TYPE_ONLINE_NUMBER,
                    'number' => $number
                ];
                $this->broadcast($server, $broadcast_data);
                break;
            case self::FINISH_DROP_LINE:
                $number = $this->getNumbers(self::ONLINE_USER);
                $broadcast_data = [
                    'type' => self::MSG_TYPE_DROP_LINE,
                    'number' => $number,
                    'fd'    => $req['fd'],
                ];
                $this->broadcast($server, $broadcast_data);
                break;
            default:
                break;
        }
    }

    /**
     * 暂未使用
     * @param $server
     * @param $fd
     * @param $code
     * @param $data
     */
    public function sendErrorMsg($server, $fd, $code, $data)
    {
        sendJson($server, $fd, ['type' => $code, 'msg' => $data]);
    }

    /**
     * 发送数据给客户端(暂未使用)
     * @param $server
     * @param $fd
     * @param $data
     */
    public function sendJson($server,$fd, $data)
    {
        $msg = json_encode($data);
        if ($server->push(intval($fd), $msg) === false)
        {
            $server->close(intval($fd));
        }
    }

    /**
     * 设置连接用户的身份
     * @param $type
     */
    private function setIdentity($server, $fd, $data)
    {
        $type_list = $this->typeToList($data['type']);
        $redis = new Redis();
        $redis->connect(self::REDIS_HOST, self::REDIS_PORT);
        $redis->select(self::REDIS_INDEX);
        $redis->sAdd($type_list, $fd);
        $redis->close();
        $server->table->set((string)$fd, $data);
        if ($data['type'] == self::CUSTOMER_PRE_IDENTITY || $data['type'] == self::CUSTOMER_AFTER_IDENTITY)
            $server->protect($fd, true);
    }

    /**
     * 客户端关闭后   删除连接信息
     * @param $server
     * @param $fd
     */
    private function delIdentity($server, $fd, $type)
    {
        $type_list = $this->typeToList($type);
        $redis = new Redis();
        $redis->connect(self::REDIS_HOST, self::REDIS_PORT);
        $redis->select(self::REDIS_INDEX);
        $redis->sRem($type_list, $fd);
        $redis->close();
        $server->table->del((string)$fd);
    }

    /**
     * 根据用户的type, 获取用户列表的类型;
     * @param $type
     * @return string
     */
    private function typeToList($type)
    {
        switch ($type) {
            case self::VIP_IDENTITY:
                $type_list = self::VIP_LIST;
                break;
            case self::CUSTOMER_PRE_IDENTITY:
                $type_list = self::CUSTOMER_PRE_LIST;
                break;
            case self::CUSTOMER_AFTER_IDENTITY:
                $type_list = self::CUSTOMER_AFTER_LIST;
                break;
            case self::NORMAL_IDENTITY:
                $type_list = self::GUEST_LIST;
                break;
            default:
                $type_list = self::GUEST_LIST;
        }
        return $type_list;
    }

    /**
     * todo 暂未处理售前客服 和 售后客服
     * 获取一个客服
     * @param $type
     * @return array|string
     */
    private function getCustomerOne($type)
    {
        if ($type == self::NORMAL_IDENTITY || $type == self::VIP_IDENTITY) {
            $redis = new Redis();
            $redis->connect(self::REDIS_HOST, self::REDIS_PORT);
            $redis->select(self::REDIS_INDEX);
            $fd = $redis->sRandMember(self::CUSTOMER_PRE_LIST);
            $redis->close();
            return $fd ? $fd : '';
        }
        return '';
    }

    /**
     * *获取在线人数
     * @param $type
     * @return int
     */
    private function getNumbers($type)
    {
        $redis = new Redis();
        $redis->connect(self::REDIS_HOST, self::REDIS_PORT);
        $redis->select(self::REDIS_INDEX);
        switch ($type) {
            case self::ONLINE_TYPE :
                $number = $redis->sCard(self::GUEST_LIST) + $redis->sCard(self::VIP_LIST) + $redis->sCard(self::CUSTOMER_PRE_LIST) + $redis->sCard(self::CUSTOMER_AFTER_LIST);
                break;
            case self::ONLINE_CUSTOMER:
                $number = $redis->sCard(self::CUSTOMER_PRE_LIST) + $redis->sCard(self::CUSTOMER_AFTER_LIST);
                break;
            case self::ONLINE_CUSTOMER_PRE:
                $number = $redis->sCard(self::CUSTOMER_PRE_LIST);
                break;
            case self::ONLINE_CUSTOMER_AFTER:
                $number = $redis->sCard(self::CUSTOMER_AFTER_LIST);
                break;
            case self::ONLINE_USER:
                $number = $redis->sCard(self::VIP_LIST) + $redis->sCard(self::GUEST_LIST);
                break;
            case self::ONLINE_GUEST:
                $number = $redis->sCard(self::GUEST_LIST);
                break;
            case self::ONLINE_VIP:
                $number = $redis->sCard(self::VIP_LIST);
                break;
            default:
                $number = 0;
        }
        $redis->close();
        return $number;
    }

    /**
     *获取聊天记录
     */
    private function getHistory()
    {

    }

    /**
     * 添加聊天记录
     */
    private function addHistory()
    {

    }

    /**
     * 获取单个用户信息
     * @param $server
     * @param $fd
     * @return mixed
     */
    private function getUser($server, $fd)
    {
        return $server->table->get((string)$fd);
    }

    /**
     * 根据fd获取在线用户的类型
     * @param $fd
     */
    private function fdToType($server, $fd)
    {
        return $server->table->get((string)$fd, 'type');
        //return $info['type'];
    }

    /**
     * 广播消息给客服
     * @param $server
     * @param $data
     */
    private function broadcast($server, $data)
    {
        $redis = new swoole_redis();
        $redis->connect(self::REDIS_HOST, self::REDIS_PORT, function (swoole_redis $redis, $result) use ($server, $data) {
            $redis->select(self::REDIS_INDEX, function (swoole_redis $redis, $result) use ($server, $data) {
                $redis->sMembers(self::CUSTOMER_PRE_LIST, function (swoole_redis $redis, $result) use ($server, $data) {
                    $redis->close();
                    if ($result) {
                        foreach ($result as $v) {
                            $server->push($v, json_encode($data));
                        }
                    }
                });
            });
        });
    }

    /**
     * 存储消息
     * @param $userFd
     * @param $toUserFd
     * @param $msg
     */
    private function add_msg($userFd, $toUserFd, $msg)
    {

    }

    /**
     * 将聊天记录写入MongoDB
     * @param $userFd
     * @param $toUserFd
     * @throws \MongoDB\Driver\Exception\Exception
     */
    private function insert_mongodb($userFd, $toUserFd, $msg)
    {
        $mongo_client = $this->get_mongo_model();
        $filter = ['name' => 'content_id'];
        $options = [];
        $namespace_ids = "swoole.ids";
        $res = $mongo_client->select($filter, $options, $namespace_ids);
        $old_id = $res[0]->id;
        $document = [
            '_id' => new MongoDB\BSON\ObjectID,
            'id' => $old_id,
            'userFd' => intval($userFd),
            'toUserFd' => intval($toUserFd),
            'msg'   => htmlspecialchars($msg),
            'create_at' => time(),
        ];

        $namespace_content = "swoole.contents";

        $res = $mongo_client->add($document, $namespace_content);
        if ($res) {
            $new_id = $old_id + 1;
            $data = ['$set' => ['id' => $new_id]];
            $options_update = ['multi' => false, 'upsert' => false];
            $mongo_client->update($filter, $data, $options_update, $namespace_ids);
        }

    }

    private function get_mongo_model()
    {
        require_once "MongoModel.php";

        return new MongoModel();

    }

    /**
     * 验证登录用户的身份（游客不用）
     */
    private function check_identity()
    {

    }

    /**
     * 添加会话记录
     */
    private function insert_session($customerFd, $userFd, $type)
    {
        $mongo_client = $this->get_mongo_model();

        $document = [
            '_id' => new MongoDB\BSON\ObjectID,
            'customerFd' => intval($customerFd),
            'userFd' => intval($userFd),
            'type'   => intval($type),
            'create_at' => time(),
        ];

        $namespace_content = "swoole.contents";

        $res = $mongo_client->add($document, $namespace_content);
    }

    /**
     * 格式化  响应的数据
     * @param $server
     * @param $requestData
     * @param $fd
     * @return array
     */
    private function format_response_data($server, $requestData, $fd)
    {
        switch ($requestData['type']) {
            case self::MSG_TYPE_IDENTITY:
                $identityData = [
                    'avatar' =>  '',
                    'nickname' => '',
                    'ip'  => '127.0.0.1',
                    'create_at' => time(),
                ];
                $user_info = [
                    'fd'   => $fd,
                    'type' => $requestData['identity_type'],
                    'data' => $identityData,
                    'task_type' => self::TASK_ADD_IDENTITY,
                    'msg_type' => $requestData['type'],
                ];
                $server->task(serialize($user_info));
                $toUserFd = $this->getCustomerOne($user_info['type']);
                $data = [
                    'userFd'    => $fd,
                    'toUserFd'  => $toUserFd,
                    'type'      => self::MSG_TYPE_IDENTITY,
                ];
                break;
            case self::MSG_TYPE_SEND:
                try {
                    $task_data = [
                        'userFd' => $requestData['userFd'],
                        'toUserFd' => $requestData['toUserFd'],
                        'task_type' => self::TASK_ADD_MONGO,
                        'msg' => $requestData['msg'],
                        'msg_type' => $requestData['type'],
                    ];
                    $server->taskwait(serialize($task_data));
                } catch (Exception $e) {

                }
                $data = [
                    'userFd'    => $requestData['toUserFd'],
                    'toUserFd'  => $requestData['userFd'],
                    'type'      => self::MSG_TYPE_SEND,
                    'msg'       => $requestData['msg'],
                ];
                break;
            case self::MSG_TYPE_FILE:
                try {
                    $task_data = [
                        'userFd' => $requestData['userFd'],
                        'toUserFd' => $requestData['toUserFd'],
                        'task_type' => self::TASK_ADD_MONGO,
                        'msg' => $requestData['msg'],
                        'url' => $requestData['url'],
                        'msg_type' => $requestData['type'],
                    ];
                    $server->taskwait(serialize($task_data));
                } catch (Exception $e) {

                }
                $data = [
                    'userFd'    => $requestData['toUserFd'],
                    'toUserFd'  => $requestData['userFd'],
                    'type'      => self::MSG_TYPE_FILE,
                    'msg'       => $requestData['msg'],
                    'url'       => $requestData['url'],
                ];
                break;
            case self::MSG_TYPE_CLOSE:
                try {
                    $task_data = [
                        'userFd' => $requestData['userFd'],
                        'toUserFd' => $requestData['toUserFd'],
                        'task_type' => self::TASK_ADD_MONGO,
                        'msg' => $requestData['msg'],
                        'url' => $requestData['url'],
                        'msg_type' => $requestData['type'],
                    ];
                    $server->taskwait(serialize($task_data));
                } catch (Exception $e) {

                }
                $data = [
                    'userFd'    => $requestData['toUserFd'],
                    'toUserFd'  => $requestData['userFd'],
                    'type'      => self::MSG_TYPE_CLOSE,
                    'msg'       => self::CHARACTER_DESCRIPTION_CLOSE,
                ];
                break;
            default:
                $data = [];
                break;
        }
        return $data;
    }
}

$obj = new Ws();